﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

namespace Midi
{

    [StructLayout(LayoutKind.Explicit)]
    struct MidiMsg
    {
        [FieldOffset(0)]
        public byte byte1;

        [FieldOffset(1)]
        public byte byte2;

        [FieldOffset(2)]
        public byte byte3;

        [FieldOffset(3)]
        public byte byte4;

        [FieldOffset(0)]
        public long dwData;

        // Constructor:
        public MidiMsg(byte b1, byte b2, byte b3)
        {
            this.dwData = 0;
            this.byte1 = b1;
            this.byte2 = b2;
            this.byte3 = b3;
            this.byte4 = 0;
        }
    }

    /// <summary>
    /// The base class for all MIDI device exception classes.
    /// </summary>
    public class MidiDeviceException // : DeviceException
    {
        #region Error Codes

        public const int MIDIERR_UNPREPARED = 64; /* header not prepared */
        public const int MIDIERR_STILLPLAYING = 65; /* still something playing */
        public const int MIDIERR_NOMAP = 66; /* no configured instruments */
        public const int MIDIERR_NOTREADY = 67; /* hardware is still busy */
        public const int MIDIERR_NODEVICE = 68; /* port no longer connected */
        public const int MIDIERR_INVALIDSETUP = 69; /* invalid MIF */
        public const int MIDIERR_BADOPENMODE = 70; /* operation unsupported w/ open mode */
        public const int MIDIERR_DONT_CONTINUE = 71; /* thru device 'eating' a message */
        public const int MIDIERR_LASTERROR = 71; /* last error in range */

        #endregion

        #region Construction

        /// <summary>
        /// Initializes a new instance of the DeviceException class with the
        /// specified error code.
        /// </summary>
        /// <param name="errCode">
        /// The error code.
        /// </param>
        public MidiDeviceException(int errCode)
            //: base(errCode)
        {
        }

        #endregion
    }

    #region Channel Command Types

    /// <summary>
    /// Defines constants for ChannelMessage types.
    /// </summary>
    public enum ChannelCommand
    {
        /// <summary>
        /// Represents the note-off command type.
        /// </summary>
        NoteOff = 0x80,

        /// <summary>
        /// Represents the note-on command type.
        /// </summary>
        NoteOn = 0x90,

        /// <summary>
        /// Represents the poly pressure (aftertouch) command type.
        /// </summary>
        PolyPressure = 0xA0,

        /// <summary>
        /// Represents the controller command type.
        /// </summary>
        Controller = 0xB0,

        /// <summary>
        /// Represents the program change command type.
        /// </summary>
        ProgramChange = 0xC0,

        /// <summary>
        /// Represents the channel pressure (aftertouch) command 
        /// type.
        /// </summary>
        ChannelPressure = 0xD0,

        /// <summary>
        /// Represents the pitch wheel command type.
        /// </summary>
        PitchWheel = 0xE0
    }

    #endregion

    /// Represents MIDI output device capabilities.
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct MidiOutCaps
    {
        #region MidiOutCaps Members

        /// <summary>
        /// Manufacturer identifier of the device driver for the Midi output 
        /// device. 
        /// </summary>
        public short mid;

        /// <summary>
        /// Product identifier of the Midi output device. 
        /// </summary>
        public short pid;

        /// <summary>
        /// Version number of the device driver for the Midi output device. The 
        /// high-order byte is the major version number, and the low-order byte 
        /// is the minor version number. 
        /// </summary>
        public int driverVersion;

        /// <summary>
        /// Product name.
        /// </summary>
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
        public string name;

        /// <summary>
        /// Flags describing the type of the Midi output device. 
        /// </summary>
        public short technology;

        /// <summary>
        /// Number of voices supported by an internal synthesizer device. If 
        /// the device is a port, this member is not meaningful and is set 
        /// to 0. 
        /// </summary>
        public short voices;

        /// <summary>
        /// Maximum number of simultaneous notes that can be played by an 
        /// internal synthesizer device. If the device is a port, this member 
        /// is not meaningful and is set to 0. 
        /// </summary>
        public short notes;

        /// <summary>
        /// Channels that an internal synthesizer device responds to, where the 
        /// least significant bit refers to channel 0 and the most significant 
        /// bit to channel 15. Port devices that transmit on all channels set 
        /// this member to 0xFFFF. 
        /// </summary>
        public short channelMask;

        /// <summary>
        /// Optional functionality supported by the device. 
        /// </summary>
        public int support;

        #endregion
    }

    static class NativeMethods
    {
        public const int CallbackFunction = 0x30000;

        public delegate void MidiOutCallback(IntPtr midiHandle, int message,
        IntPtr userData, int param1, int param2);

        [DllImport("winmm.dll")]
        public static extern int midiOutClose(IntPtr midiHandle);

        [DllImport("winmm.dll")]
        public static extern int midiOutOpen(out MidiSafeHandle midiHandle, int deviceId, MidiOutCallback callback, IntPtr userData, int flags);

        [DllImport("winmm.dll")]
        public static extern int midiOutShortMsg(MidiSafeHandle midiHandle, long dwMsg);

        [DllImport("winmm.dll")]
        public static extern int midiOutGetNumDevs();

        [DllImport("winmm.dll")]
        public static extern int midiOutGetDevCaps(int deviceID, ref MidiOutCaps caps, int sizeOfMidiOutCaps);


    }

    class MidiSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public MidiSafeHandle()
            : base(true)
        { }

        protected override bool ReleaseHandle()
        {
            bool success = (NativeMethods.midiOutClose(this.handle) == 0);
            handle = IntPtr.Zero;
            return success;
        }
    }

    public class MidiOutPort : IDisposable
    {
        private GCHandle _gcHandle;
        private MidiSafeHandle _midiHandle;

        public MidiOutPort()
        {
            _gcHandle = GCHandle.Alloc(this, GCHandleType.Weak);
        }

        public void Open(int device)
        {
            int result = NativeMethods.midiOutOpen(
            out _midiHandle, device, _callback, GCHandle.ToIntPtr(_gcHandle),
            NativeMethods.CallbackFunction);
            if (result != 0)
            {
                Console.WriteLine("Error opening MIDI port");
            }
        }

        public void SendMsg(Byte byte1, Byte byte2, Byte byte3 = 0)
        {
            MidiMsg msg = new MidiMsg(byte1, byte2, byte3);
            //Console.WriteLine("Msg={0:x}", msg.dwData);
            int result = NativeMethods.midiOutShortMsg(_midiHandle, msg.dwData);
        }

        public MidiOutCaps GetDeviceCapabilities(int deviceID)
        {
            MidiOutCaps caps = new MidiOutCaps();

            // Get the device's capabilities.
            int result = NativeMethods.midiOutGetDevCaps(deviceID, ref caps, Marshal.SizeOf(caps));

            // If the capabilities could not be retrieved.
            if (result != 0)
            {
                Console.WriteLine("Error in getting device details");
            }

            return caps;
        }

        public int GetNumDevs()
        {
            return NativeMethods.midiOutGetNumDevs();
        }

        public void Close()
        {
            if (_midiHandle != null)
            {
                _midiHandle.Close();
            }
        }

        public void Dispose()
        {
            _gcHandle.Free();
            _midiHandle.Dispose();
        }

        private void OnMessage(int message, int param1, int param2)
        {
            Console.WriteLine(String.Format("OnMessage Message:{0} Param1:{1} Param2:{2}",
            message, param1, param2), "MidiOutPort");
        }

        private static NativeMethods.MidiOutCallback _callback = OnCallback;

        private static void OnCallback(IntPtr midiInHandle, int message, IntPtr userData, int param1, int param2)
        {
            try
            {
                GCHandle handle = GCHandle.FromIntPtr(userData);

                if (handle.Target != null)
                {
                    ((MidiOutPort)handle.Target).OnMessage(message, param1, param2);
                }
            }
            catch (Exception e)
            {
                // TODO: handle exception
                Console.WriteLine(e);
            }
        }
    }
}
